1. Introduction
When we use XML with Java, we can approach it as just some text we want to read manipulate and write to some external destination. If that is the case we sometimes can limit ourselves to concatenating some strings or use an in-memory DOM representation to access, create or modify a document.
Most of the time, however, we have our own set of classes - a model, beans or POJOs, whatever you like to call them - that we want to write or represent in an XML document and later retrieve it back from that representation. E.g. to store it in a file, send it via a webservice, etc.
This process is binding a Java set of objects to an XML representation of them. Creating objects from XML is unmarshalling and the reverse, generating XML from the Java objects is marshalling .
There are different ways to do this:
- Write a SAX parser and create Java objects as you go
- Parse a DOM tree, and create your particular objects from them
- Use a specialized API such as JAXB, Apache Commons Betwixt, etc.
Even Spring IOC can in some ways be seen as doing this, but that's another topic...
2. Java Architecture for XML Binding
The JAXB is not just an API, there are tools to generate Java classes to form a binding with XML, from a Schema or to create a schema from annotated Java classes.
2.1. A Practical Example - Annotated Java Classes
A reasonable use case for JAXB is when we have a set of classes in an application that we want to write to and read from XML. Here we will use a very simple case of modeling a group of people. We have two classes Group and People , besides that we have a class that uses JAXB to generate XML and read from XML: Application
In Java 6 we use standard APIs available, nothing else needs to be done, no configurations or generation of schemas etc.
Here is the root class for our XML document, we have to annotate as such with the @XmlRootElement annotation.The propOrder = { "name", "members" } part indicates in which order the XML elements will appear.
package be.ooxs.example.jaxb; import java.util.ArrayList; import java.util.List; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; @XmlType(propOrder = { "name", "members" }, name = "group") @XmlRootElement public class Group { private String name; private List<Person> members = new ArrayList<Person>(); public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Person> getMembers() { return members; } public void setMembers(List<Person> members) { this.members = members; } }
We have a similar treatment of the child class in our object graph:
package be.ooxs.example.jaxb; import java.util.Date; import javax.xml.bind.annotation.XmlType; @XmlType(propOrder = { "lastName", "firstName", "birthDate" }, name = "person") public class Person { private Date birthDate; private String firstName; private String lastName; public Date getBirthDate() { return birthDate; } public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
And here is our "Application" that depends on the serialization (marshalling-unmarshalling) of these classes with JAXB.
package be.ooxs.example.jaxb; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; public class Application { private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); public Group createJavaObjectExample1() { Group group = new Group(); group.setName("Test Group 1"); try { group.getMembers().add(createPerson("Alice", "Anderssen", "1970-01-01")); group.getMembers().add(createPerson("Bert", "Bobo", "1980-02-02")); } catch (ParseException exception) { Logger.getLogger(Application.class.getName()).log(Level.ALL, "createJavaObjectExample1 threw ParseException", exception); } return group; } public Person createPerson(String firstName, String lastName, String birthDate) throws ParseException { Person person = new Person(); person.setBirthDate(format.parse(birthDate)); person.setFirstName(firstName); person.setLastName(lastName); return person; } public void marshallExample() { //... } public void unmarshallExample() { //... } public static void main(String[] args) { Application instance = new Application(); instance.marshallExample(); instance.unmarshallExample(); System.out.println("Done"); } }
Run this Application class and some objects (a group with two members) are created, just to demonstrate the JAXB API's.
First, take a look at how you can generate some XML:
public void marshallExample() { try { JAXBContext context = JAXBContext.newInstance(Group.class); Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.marshal(createJavaObjectExample1(), System.out); } catch (JAXBException exception) { Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "marshallExample threw JAXBException", exception); } }
Thanks to the annotations, it only takes three lines of code to generate the XML. OK, four if you want it indented for more human reading comfort.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <group> <name>Test Group 1</name> <members> <lastName>Anderssen</lastName> <firstName>Alice</firstName> <birthDate>1970-01-01T00:00:00+01:00</birthDate> </members> <members> <lastName>Bobo</lastName> <firstName>Bert</firstName> <birthDate>1980-02-02T00:00:00+01:00</birthDate> </members> </group>
Still, we have a few unexpected element names and we will resolve that a little later .
Each Person is surrounded with a <members> tag, instead of <person> . Which is of course derived from the collection accessor in Group .
Now look at the reverse operation, using XML and reconstructing the objects. First we will repeat the previous steps, just to create some XML, we then write it to a byte array. The next three lines of code actually do the unmarshalling. Nice.
And then just to prove the point, we print a little information about the three objects.
public void unmarshallExample() { try { JAXBContext context = JAXBContext.newInstance(Group.class); Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); //write XML to an array of bytes ByteArrayOutputStream baos = new ByteArrayOutputStream(); marshaller.marshal(createJavaObjectExample1(), baos); //read XML from array of bytes InputStream bais = new ByteArrayInputStream(baos.toByteArray()); Unmarshaller unmarshaller = context.createUnmarshaller(); Object o = unmarshaller.unmarshal(bais); //prove the Group is recreated Group groupCopy = (Group) o; System.out.println("Objects created from XML:"); System.out.println(groupCopy.getName()); for (Person person : groupCopy.getMembers()) { System.out.println(person.getFirstName()); } } catch (JAXBException exception) { Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "marshallExample threw JAXBException", exception); } }
When you have Java 6 SDK installed, JAXB tools are available at $JAVA_HOME/bin .
With Linux distributions like Ubuntu, these are by default available in the shell or 'command prompt' if you like.
:~$ mkdir temp/ :~$ mkdir temp/sg :~$ schemagen -d /home/user/temp/sg -cp /home/user/path/to/source be.ooxs.example.jaxb.Group Note: Writing /home/user/temp/sg/schema1.xsd :~$ mv /home/user/temp/sg/schema1.xsd /home/user/temp/sg/group-schema.xsd
3. Generating an XML Schema
For Windows users, you will probably have to specify where to find the tools or add them to your %PATH% variable.
For Mac OSX (10.5) users, the default JDK is version 5, therefore the schemagen tool is not directly available. However typing locate bin/schemagen in a terminal, will reveal the location.
This results in an schema file that you can use as required. Remark that the order of the elements is "lastName", "firstName", "birthDatee" as defined in the annotations.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:complexType name="group"> <xs:sequence> <xs:element name="name" type="xs:string" minOccurs="0"/> <xs:element name="members" type="person" nillable="true" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> </xs:complexType> <xs:complexType name="person"> <xs:sequence> <xs:element name="lastName" type="xs:string" minOccurs="0"/> <xs:element name="firstName" type="xs:string" minOccurs="0"/> <xs:element name="birthDate" type="xs:dateTime" minOccurs="0"/> </xs:sequence> </xs:complexType> </xs:schema>
4. Improved Annotations
In order to have a slightly better XML structure, let's change a few things:
First add two annotations, the first will group all the children using an extra <members> element, the second changes each child to <person>
@XmlElementWrapper(name = "members") @XmlElement(name = "person") public List<Person> getMembers() { return members; }
And this is the resulting XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <group> <name>Test Group 1</name> <members> <person> <lastName>Anderssen</lastName> <firstName>Alice</firstName> <birthDate>1970-01-01T00:00:00+01:00</birthDate> </person> <person> <lastName>Bobo</lastName> <firstName>Bert</firstName> <birthDate>1980-02-02T00:00:00+01:00</birthDate> </person> </members> </group>
That's much better, isn't it?
And here is the new XML Schema:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="group" type="group"/> <xs:complexType name="group"> <xs:sequence> <xs:element name="name" type="xs:string" minOccurs="0"/> <xs:element name="members" minOccurs="0"> <xs:complexType> <xs:sequence> <xs:element name="person" type="person" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="person"> <xs:sequence> <xs:element name="lastName" type="xs:string" minOccurs="0"/> <xs:element name="firstName" type="xs:string" minOccurs="0"/> <xs:element name="birthDate" type="xs:dateTime" minOccurs="0"/> </xs:sequence> </xs:complexType> </xs:schema>
5. More Elaborate Example
Now, to show some more possibilities of the JAXB annotations, we will introduce two enums and use them as properties for our Person .
package be.ooxs.example.jaxb.extended; import java.util.Date; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlType; @XmlType(propOrder = { "lastName", "firstName", "birthDate", "type" }, name = "person") public class Person { private Date birthDate; private String firstName; private String lastName; private PersonType type; @XmlAttribute(name = "active") private Status status = Status.PENDING_APPROVAL; public PersonType getType() { return type; } public void setType(PersonType type) { this.type = type; } public Person delete() { status = Status.INACTIVE; return this; } public Person approve() { status = Status.ACTIVE; return this; } public Date getBirthDate() { return birthDate; } public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
Remark that we don't necessarily have to have getters and setters for the properties. In this case, Status is something internal that changes through very complicated business logic. OK, not that complicated, but you get the idea.
Here is the PersonType enum...
package be.ooxs.example.jaxb.extended; public enum PersonType { CUSTOMER, EMPLOYEE, MANAGER }
...and here is the Status enum.
package be.ooxs.example.jaxb.extended; import javax.xml.bind.annotation.XmlEnumValue; public enum Status { @XmlEnumValue(value = "pending") PENDING_APPROVAL, @XmlEnumValue(value = "active") ACTIVE, @XmlEnumValue(value = "inactive") INACTIVE }
Now, since we're improving things, why not improve our date parsing? We can drop everything but the year, month an day. To achieve this we implement an adapter class:
package be.ooxs.example.jaxb.extended; import java.text.SimpleDateFormat; import java.util.Date; import javax.xml.bind.annotation.adapters.XmlAdapter; public class ShortDateFormatter extends XmlAdapter<String, Date> { SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); public Date unmarshal(String date) throws Exception { return formatter.parse(date); } public String marshal(Date date) throws Exception { return formatter.format(date); } }
And then annotate the getBirthDate() accessor.
... @XmlJavaTypeAdapter(ShortDateFormatter.class) public Date getBirthDate() { return birthDate; } ...
Do not annotate the field, in this case. If you try that the runtime will throw an Exception, since both the field an the accessor yield a property with the same name: birthDate.
You can suppress this with a @XmlAccessorType(XmlAccessType.FIELD) at the class level, but that affects how values are accessed for the entire class, not the best approach. This is the exception and it mentions what has happened - but you have to look in the right places.
com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException: 2 counts of IllegalAnnotationExceptions There are two properties named "birthDate" this problem is related to the following location: at public java.util.Date be.ooxs.example.jaxb.extended.Person.getBirthDate() ... this problem is related to the following location: at private java.util.Date be.ooxs.example.jaxb.extended.Person.birthDate
As you can see, we mapped the Status to different values: "pending" instead of "PENDING_APPROVAL", respecting Java and XML conventions. And just to show how it can be done, as an XML attribute instead of as a text child.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <group> <name>Test Group 2</name> <members> <person active="active"> <lastName>Anderssen</lastName> <firstName>Alice</firstName> <birthDate>1970-01-01</birthDate> <type>CUSTOMER</type> </person> <person active="inactive"> <lastName>Bobo</lastName> <firstName>Bert</firstName> <birthDate>1980-02-02</birthDate> <type>EMPLOYEE</type> </person> </members> </group>
The generated XML Schema is aware of the limited values allowed for the enums:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="group" type="group"/> <xs:complexType name="group"> <xs:sequence> <xs:element name="name" type="xs:string" minOccurs="0"/> <xs:element name="members" minOccurs="0"> <xs:complexType> <xs:sequence> <xs:element name="person" type="person" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="person"> <xs:sequence> <xs:element name="lastName" type="xs:string" minOccurs="0"/> <xs:element name="firstName" type="xs:string" minOccurs="0"/> <xs:element name="birthDate" type="xs:string" minOccurs="0"/> <xs:element name="type" type="personType" minOccurs="0"/> </xs:sequence> <xs:attribute name="active" type="status"/> </xs:complexType> <xs:simpleType name="personType"> <xs:restriction base="xs:string"> <xs:enumeration value="CUSTOMER"/> <xs:enumeration value="EMPLOYEE"/> <xs:enumeration value="MANAGER"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="status"> <xs:restriction base="xs:string"> <xs:enumeration value="pending"/> <xs:enumeration value="active"/> <xs:enumeration value="inactive"/> </xs:restriction> </xs:simpleType> </xs:schema>